﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace MetaMapics.Pbf
{
  public class PbfWriter: IDisposable
  {
    private Stream stream;

    public PbfWriter(Stream stream)
    {
      this.stream = stream;
    }

    #region Standard Types
    public void Write(uint fieldNum, double value)
    {
      PbfHelper.WriteTag(stream, fieldNum, PbfWireType.Bits64);
      PbfHelper.WriteDouble(stream, value);
    }

    public void Write(uint fieldNum, float value)
    {
      PbfHelper.WriteTag(stream, fieldNum, PbfWireType.Bits32);
      byte[] bytes = BitConverter.GetBytes(value);
      for (int i = 0; i < 4; i++) //?? is this the right way round?
        stream.WriteByte(bytes[i]);
    }

    public void Write(uint fieldNum, int value)
    {
      Write(fieldNum, value, PbfFieldType.Int32);
    }

    public void Write(uint fieldNum, uint value)
    {
      Write(fieldNum, value, PbfFieldType.Uint32);
    }

    public void Write(uint fieldNum, long value)
    {
      Write(fieldNum, value, PbfFieldType.Int64);
    }

    public void Write(uint fieldNum, ulong value)
    {
      Write(fieldNum, value, PbfFieldType.Uint64);
    }

    public void Write(uint fieldNum, bool value)
    {
      PbfHelper.WriteTag(stream, fieldNum, PbfWireType.VarInt);
      PbfHelper.WriteBool(stream, value);
    }

    public void Write(uint fieldNum, string value)
    {
      PbfHelper.WriteTag(stream, fieldNum, PbfWireType.LengthDelimited);
      byte[] bytes = Encoding.UTF8.GetBytes(value);
      PbfHelper.WriteVarint(stream, (uint)bytes.Length);
      for (int i = 0; i < bytes.Length; i++)
        stream.WriteByte(bytes[i]);
    }

    public void Write(uint fieldNum, byte[] bytes)
    {
      PbfHelper.WriteTag(stream, fieldNum, PbfWireType.LengthDelimited);
      PbfHelper.WriteVarint(stream, (uint)bytes.Length);
      for (int i = 0; i < bytes.Length; i++)
        stream.WriteByte(bytes[i]);
    }
    #endregion

    #region Packed Standard Types
    public void WritePacked(uint fieldNum, double[] values)
    {
      PbfHelper.WriteTag(stream, fieldNum, PbfWireType.LengthDelimited);
      var buffer = new byte[values.Length * 10];
      uint byteCount = 0;
      using (var memStream = new MemoryStream(buffer))
      {
        for (int i = 0; i < values.Length; i++)
          PbfHelper.WriteDouble(memStream, values[i]);
        byteCount = (uint)memStream.Position;
      }
      PbfHelper.WriteVarint(stream, byteCount);
      for (int i = 0; i < byteCount; i++)
        stream.WriteByte(buffer[i]);
    }

    public void WritePacked(uint fieldNum, float[] values)
    {
      PbfHelper.WriteTag(stream, fieldNum, PbfWireType.LengthDelimited);
      var buffer = new byte[values.Length * 5];
      uint byteCount = 0;
      using (var memStream = new MemoryStream(buffer))
      {
        for (int i = 0; i < values.Length; i++)
          PbfHelper.WriteFloat(memStream, values[i]);
        byteCount = (uint)memStream.Position;
      }
      PbfHelper.WriteVarint(stream, byteCount);
      for (int i = 0; i < byteCount; i++)
        stream.WriteByte(buffer[i]);
    }

    public void WritePacked(uint fieldNum, int[] values)
    {
      PbfHelper.WriteTag(stream, fieldNum, PbfWireType.LengthDelimited);
      var buffer = new byte[values.Length * 5];
      uint byteCount = 0;
      using (var memStream = new MemoryStream(buffer))
      {
        for (int i = 0; i < values.Length; i++)
          PbfHelper.WriteVarint(memStream, (uint)values[i]);
        byteCount = (uint)memStream.Position;
      }
      PbfHelper.WriteVarint(stream, byteCount);
      for (int i = 0; i < byteCount; i++)
        stream.WriteByte(buffer[i]);
    }

    public void WritePacked(uint fieldNum, uint[] values)
    {
      PbfHelper.WriteTag(stream, fieldNum, PbfWireType.LengthDelimited);
      var buffer = new byte[values.Length * 5];
      uint byteCount = 0;
      using (var memStream = new MemoryStream(buffer))
      {
        for (int i = 0; i < values.Length; i++)
          PbfHelper.WriteVarint(memStream, values[i]);
        byteCount = (uint)memStream.Position;
      }
      PbfHelper.WriteVarint(stream, byteCount);
      for (int i = 0; i < byteCount; i++)
        stream.WriteByte(buffer[i]);
    }

    public void WritePacked(uint fieldNum, long[] values)
    {
      PbfHelper.WriteTag(stream, fieldNum, PbfWireType.LengthDelimited);
      var buffer = new byte[values.Length * 10];
      uint byteCount = 0;
      using (var memStream = new MemoryStream(buffer))
      {
        for (int i = 0; i < values.Length; i++)
          PbfHelper.WriteVarint(memStream, (ulong)values[i]);
        byteCount = (uint)memStream.Position;
      }
      PbfHelper.WriteVarint(stream, byteCount);
      for (int i = 0; i < byteCount; i++)
        stream.WriteByte(buffer[i]);
    }

    public void WritePacked(uint fieldNum, ulong[] values)
    {
      PbfHelper.WriteTag(stream, fieldNum, PbfWireType.LengthDelimited);
      var buffer = new byte[values.Length * 10];
      uint byteCount = 0;
      using (var memStream = new MemoryStream(buffer))
      {
        for (int i = 0; i < values.Length; i++)
          PbfHelper.WriteVarint(memStream, values[i]);
        byteCount = (uint)memStream.Position;
      }
      PbfHelper.WriteVarint(stream, byteCount);
      for (int i = 0; i < byteCount; i++)
        stream.WriteByte(buffer[i]);
    }

    public void WritePacked(uint fieldNum, bool[] values)
    {
      PbfHelper.WriteTag(stream, fieldNum, PbfWireType.LengthDelimited);
      PbfHelper.WriteVarint(stream, values.Length);
      for (int i = 0; i < values.Length; i++)
        stream.WriteByte((byte)(values[i]?1:0));
    }
    #endregion

    #region Alt Types
    public void Write(uint fieldNum, int value, PbfFieldType fieldType)
    {
      if (fieldType == PbfFieldType.Sint32)
      {
        PbfHelper.WriteTag(stream, fieldNum, PbfWireType.VarInt);
        PbfHelper.WriteVarint(stream, PbfHelper.EncodeZigZag(value));
      }
      else if (fieldType == PbfFieldType.Sfixed32)
      {
        PbfHelper.WriteTag(stream, fieldNum, PbfWireType.Bits32);
        byte[] bytes = BitConverter.GetBytes(value);
        for (int i = 0; i < 4; i++)
          stream.WriteByte(bytes[i]);
      }  
      else if (fieldType == PbfFieldType.Int32)
      {
        PbfHelper.WriteTag(stream, fieldNum, PbfWireType.VarInt);
        PbfHelper.WriteVarint(stream, (uint)value);
      }
      else
        throw new ArgumentException();
    }

    public void Write(uint fieldNum, long value, PbfFieldType fieldType)
    {
      if (fieldType == PbfFieldType.Sint64)
      {
        PbfHelper.WriteTag(stream, fieldNum, PbfWireType.VarInt);
        PbfHelper.WriteVarint(stream, PbfHelper.EncodeZigZag(value));
      }
      else if (fieldType == PbfFieldType.Sfixed64)
      {
        PbfHelper.WriteTag(stream, fieldNum, PbfWireType.Bits32);
        byte[] bytes = BitConverter.GetBytes(value);
        for (int i = 0; i < 8; i++)
          stream.WriteByte(bytes[i]);
      }  
      else if (fieldType == PbfFieldType.Int64)
      {
        PbfHelper.WriteTag(stream, fieldNum, PbfWireType.VarInt);
        PbfHelper.WriteVarint(stream, (ulong)value);
      }
      else
        throw new ArgumentException();
    }

    public void Write(uint fieldNum, uint value, PbfFieldType fieldType)
    {
      if (fieldType == PbfFieldType.Fixed32)
      {
        PbfHelper.WriteTag(stream, fieldNum, PbfWireType.Bits32);
        byte[] bytes = BitConverter.GetBytes(value);
        for (int i = 0; i < 4; i++)
          stream.WriteByte(bytes[i]);
      }  
      else if (fieldType == PbfFieldType.Uint32)
      {
        PbfHelper.WriteTag(stream, fieldNum, PbfWireType.VarInt);
        PbfHelper.WriteVarint(stream, value);
      }
      else
        throw new ArgumentException();
    }

    public void Write(uint fieldNum, ulong value, PbfFieldType fieldType)
    {
      if (fieldType == PbfFieldType.Fixed64)
      {
        PbfHelper.WriteTag(stream, fieldNum, PbfWireType.Bits32);
        byte[] bytes = BitConverter.GetBytes(value);
        for (int i = 0; i < 8; i++)
          stream.WriteByte(bytes[i]);
      }  
      else if (fieldType == PbfFieldType.Uint64)
      {
        PbfHelper.WriteTag(stream, fieldNum, PbfWireType.VarInt);
        PbfHelper.WriteVarint(stream, value);
      }
      else
        throw new ArgumentException();
    }
    #endregion

    #region Packed Alt Types
    public void WritePacked(uint fieldNum, int[] values, PbfFieldType fieldType)
    {
      if (fieldType == PbfFieldType.Int32)
        WritePacked(fieldNum, values);
      else if (fieldType == PbfFieldType.Sfixed32)
      {
        PbfHelper.WriteTag(stream, fieldNum, PbfWireType.LengthDelimited);
        PbfHelper.WriteVarint(stream, values.Length * 4);
        for (int i = 0; i < values.Length; i++)
          PbfHelper.WriteBits32(stream, values[i]);
      }
      else if (fieldType == PbfFieldType.Sint32)
      {
        PbfHelper.WriteTag(stream, fieldNum, PbfWireType.LengthDelimited);
        var buffer = new byte[values.Length * 5];
        uint byteCount = 0;
        using (var memStream = new MemoryStream(buffer))
        {
          for (int i = 0; i < values.Length; i++)
            PbfHelper.WriteVarint(memStream, PbfHelper.EncodeZigZag(values[i]));
          byteCount = (uint)memStream.Position;
        }
        PbfHelper.WriteVarint(stream, byteCount);
        for (int i = 0; i < byteCount; i++)
          stream.WriteByte(buffer[i]);
      }
      else
        throw new ArgumentException();
    }

    public void WritePacked(uint fieldNum, uint[] values, PbfFieldType fieldType)
    {
      if (fieldType == PbfFieldType.Int32)
        WritePacked(fieldNum, values);
      else if (fieldType == PbfFieldType.Fixed32)
      {
        PbfHelper.WriteTag(stream, fieldNum, PbfWireType.LengthDelimited);
        PbfHelper.WriteVarint(stream, values.Length * 4);
        for (int i = 0; i < values.Length; i++)
          PbfHelper.WriteBits32(stream, (int)values[i]);
      }
      else
        throw new ArgumentException();
    }

    public void WritePacked(uint fieldNum, long[] values, PbfFieldType fieldType)
    {
      if (fieldType == PbfFieldType.Int64)
        WritePacked(fieldNum, values);
      else if (fieldType == PbfFieldType.Sfixed64)
      {
        PbfHelper.WriteTag(stream, fieldNum, PbfWireType.LengthDelimited);
        PbfHelper.WriteVarint(stream, values.Length * 8);
        for (int i = 0; i < values.Length; i++)
          PbfHelper.WriteBits64(stream, values[i]);
      }
      else if (fieldType == PbfFieldType.Sint64)
      {
        PbfHelper.WriteTag(stream, fieldNum, PbfWireType.LengthDelimited);
        var buffer = new byte[values.Length * 10];
        uint byteCount = 0;
        using (var memStream = new MemoryStream(buffer))
        {
          for (int i = 0; i < values.Length; i++)
            PbfHelper.WriteVarint(memStream, PbfHelper.EncodeZigZag(values[i]));
          byteCount = (uint)memStream.Position;
        }
        PbfHelper.WriteVarint(stream, byteCount);
        for (int i = 0; i < byteCount; i++)
          stream.WriteByte(buffer[i]);
      }
      else
        throw new ArgumentException();
    }

    public void WritePacked(uint fieldNum, ulong[] values, PbfFieldType fieldType)
    {
      if (fieldType == PbfFieldType.Int64)
        WritePacked(fieldNum, values);
      else if (fieldType == PbfFieldType.Fixed64)
      {
        PbfHelper.WriteTag(stream, fieldNum, PbfWireType.LengthDelimited);
        PbfHelper.WriteVarint(stream, values.Length * 8);
        for (int i = 0; i < values.Length; i++)
          PbfHelper.WriteBits64(stream, (int)values[i]);
      }
      else
        throw new ArgumentException();
    }
    #endregion

    #region Other Methods
    public void Write(uint fieldNum, byte[] bytes, int start, int count)
    {
      PbfHelper.WriteTag(stream, fieldNum, PbfWireType.LengthDelimited);
      PbfHelper.WriteVarint(stream, (uint)count);
      for (int i = 0; i < count; i++)
        stream.WriteByte(bytes[start + i]);
    }

    public void WritePacked(uint fieldNum, ICollection<long> values, PbfFieldType fieldType)
    {
      if (fieldType != PbfFieldType.Sfixed64)
        throw new NotImplementedException();
      PbfHelper.WriteTag(stream, fieldNum, PbfWireType.LengthDelimited);
      PbfHelper.WriteVarint(stream, (ulong)(values.Count * 8));
      foreach (var value in values)
        PbfHelper.WriteBits64(stream, value);
    }

    public void Write(uint fieldNum, IPbfStreamable dataObject)
    {
      using (var memStream = new MemoryStream())
      using (var memStreamWriter = new PbfWriter(memStream))
      {
        dataObject.WriteToPbf(memStreamWriter);
        WriteNestedMessage(fieldNum, memStream);
      }
    }

    //public void Write(uint fieldNum, List<IPbfStreamable> dataObjects)
    //{
    //  foreach (var dataObject in dataObjects)
    //    Write(fieldNum, dataObject);
    //}

    public void WriteNestedMessage(uint fieldNum, IPbfStreamable dataObject) //?? This can be depracated?
    {
      using (var memStream = new MemoryStream())
      using (var memStreamWriter = new PbfWriter(memStream))
      {
        dataObject.WriteToPbf(memStreamWriter);
        WriteNestedMessage(fieldNum, memStream);
      }
    }

    public void WriteNestedMessage(uint fieldNum, MemoryStream memStream)
    {
      PbfHelper.WriteTag(stream, fieldNum, PbfWireType.LengthDelimited);
      long byteCount = memStream.Position;
      PbfHelper.WriteVarint(stream, (uint)byteCount);
      memStream.Position = 0;
      for (int i = 0; i < byteCount; i++)
        stream.WriteByte((byte)memStream.ReadByte());
    }

    public void Dispose()
    {
      if (stream != null)
        stream.Dispose();
      stream = null;
    }

    public void Close()
    {
      Dispose();
    }
    #endregion
  }
}
